﻿using System;
using System.IO;
using System.Linq;
using FirstFloor.Documents.IO;
using FirstFloor.Documents.Imaging;

namespace FirstFloor.Documents.Pdf.Images
{
    /// <summary>
    /// Provides image decoding functionality.
    /// </summary>
    internal static class PdfImageDecoder
    {
        /// <summary>
        /// Gets the image stream for given image stream object.
        /// </summary>
        /// <param name="image">The image.</param>
        /// <returns></returns>
        public static Stream GetImageStream(StreamObject image)
        {
            var filter = image.Filters.FirstOrDefault();

            if (filter == null || filter == "FlateDecode" || filter == "CCITTFaxDecode") {
                var width = image.Dictionary.Get<int>("Width");
                var height = image.Dictionary.Get<int>("Height");
                var stream = image.OpenFilteredStream();

                if (width * height * 3 == stream.Length) {
                    // RGB
                    return GetRgbStream(image.Dictionary, width, height, stream);
                }
                else if (stream.Length == (((width + ((8 - (width % 8)) % 8)) * height) / 8)) {
                    // Monochrome
                    return GetMonochromeStream(image.Dictionary, width, height, stream);
                }
                else {
                    var bitsPerComponent = image.Dictionary.Get<int>("BitsPerComponent", -1);
                    var cs = image.Dictionary.Get<object>("ColorSpace", null);
                    var colorSpace = cs as string;
                    if (colorSpace == "DeviceGray" && bitsPerComponent == 8) {
                        // DeviceGray
                        return GetDeviceGrayStream(image.Dictionary, width, height, stream);
                    }

                    var colorSpaceArr = cs as ArrayObject;
                    if (colorSpaceArr != null && colorSpaceArr.Count == 4 && colorSpaceArr[0] as string == "Indexed") {
                        // Indexed
                        return GetIndexedStream(image.Dictionary, width, height, stream);
                    }
                }
            }
            else if (filter == "DCTDecode") {
                return image;   // return as-is
            }

            return Stream.Null;     // no image
        }

        private static Stream GetRgbStream(DictionaryObject imageDictionary, int width, int height, Stream stream)
        {
            // TODO: implement color key masking
            //var maskArr = imageDictionary.Get<object>("Mask", null) as ArrayObject;
            //byte[] mask = null;
            //if (maskArr != null && maskArr.Count == 6) {
            //    mask = maskArr.ToArray<byte>();
            //}
            // for now simple wrap RGB stream (Alpha channel not supported)
            var encoder = new BitmapEncoder();
            return encoder.Encode(width, height, stream);
        }

        private static Stream GetMonochromeStream(DictionaryObject imageDictionary, int width, int height, Stream stream)
        {
            try {
                var pixels = new int[width * height];

                for (var y = 0; y < height; y++) {
                    for (var x = 0; x < width; x+=8) {
                        var value = stream.ReadByte();

                        for (var p = 0; p < 8; p++) {
                            if (x + p < width && ((value >> (7 - p)) & 1) != 0) {
                                pixels[x + p + (y * width)] = 0xff << 24 | 0xff << 16 | 0xff << 8 | 0xff;
                            }
                        }
                    }
                }

                var encoder = new BitmapEncoder();
                return encoder.Encode(width, height, pixels, PixelFormat.Argb, 3);
            }
            finally {
                stream.Dispose();
            }
        }

        private static Stream GetDeviceGrayStream(DictionaryObject imageDictionary, int width, int height, Stream stream)
        {
            try {
                var pixels = new int[width * height];

                for (var y = 0; y < height; y++) {
                    for (var x = 0; x < width; x++) {
                        var value = stream.ReadByte();

                        pixels[x + (y * width)] = 0xff << 24 | value << 16 | value << 8 | value;
                    }
                }

                var encoder = new BitmapEncoder();
                return encoder.Encode(width, height, pixels, PixelFormat.Argb, 3);
            }
            finally {
                stream.Dispose();
            }
        }

        private static Stream GetIndexedStream(DictionaryObject imageDictionary, int width, int height, Stream stream)
        {
            try {
                var colorSpace = imageDictionary.Get<ArrayObject>("ColorSpace");
                byte[] palette;

                var paletteDict = colorSpace.Get<DictionaryObject>(3);
                using (var palleteStream = paletteDict.Owner.ParseStreamObject(paletteDict)) {
                    using (var filteredStream = palleteStream.OpenFilteredStream()) {
                        palette = filteredStream.ReadBytes((int)filteredStream.Length);
                    }
                }

                var pixels = new int[width * height];

                for (var y = 0; y < height; y++) {
                    for (var x = 0; x < width; x++) {
                        var index = stream.ReadByte() * 3;
                        var r = palette[index];
                        var g = palette[index + 1];
                        var b = palette[index + 2];

                        pixels[x + (y * width)] = 0xff << 24 | r << 16 | g << 8 | b;
                    }
                }

                var encoder = new BitmapEncoder();
                return encoder.Encode(width, height, pixels, PixelFormat.Argb, 3);
            }
            finally {
                stream.Dispose();
            }
        }

        private static byte[] ReadImageData(StreamObject image)
        {
            byte[] data;
            using (var stream = image.OpenFilteredStream()) {
                data = new byte[stream.Length];
                stream.Read(data, 0, data.Length);
            }
            return data;
        }
    }
}
